adb 指令相信大家都用得不少,但是自定义 adb 指令不知道大家又试过没有?最近公司有一个需求,需要自定义 adb 指令来对手机硬件进行测试,这篇博客我们就来一起聊一聊我的实现方法,希望能帮助到有相似需求的朋友。
我们先来看看常用的 adb 指令,比如启动活动和服务等:
启动活动: adb shell am start
启动服务: adb shell am startservice
具体指令和参数可参考官方文档:Android Debug Bridge (adb)
上面的指令中,我们通过 am 指令调用 AMS 的功能。该是放置在 system/bin 目录上的,我们可以将其打开:
#!/system/bin/sh
if [ "$1" != "instrument" ] ; then
cmd activity "$@"
else
base=/system
export CLASSPATH=$base/framework/am.jar
exec app_process $base/bin com.android.commands.am.Am "$@"
fi
可以发现,这其实就是一个 shell 文件,在该文件中通过 shell 语言调用 cmd activity 指令的功能,从而调用 AMS 的功能。在这里我们不过多去分析 am 指令的实现原理,因为重点并不在此。至于 shell 语言,我建议大家可以去 Shell 教程 稍微学一下,它其实并不难,特别是对于有编程基础的大家来说。
那么 am 文件是如何编译进手机目录的呢?源码的位置位于 frameworks\base\cmds\am,在这里面有一个 am 文件,和上面的代码是一模一样的。除此之外我们还能发现一些 src 文件夹和其他一些文件,其中我们需要重点关注的是 Android.mk 这个文件。
# Copyright 2008 The Android Open Source Project
#
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
$(call all-java-files-under, src) \
$(call all-proto-files-under, proto)
LOCAL_MODULE := am
LOCAL_PROTOC_OPTIMIZE_TYPE := stream
include $(BUILD_JAVA_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := am
LOCAL_SRC_FILES := am
LOCAL_MODULE_CLASS := EXECUTABLES
LOCAL_MODULE_TAGS := optional
include $(BUILD_PREBUILT)
上面的 mk 文件主要是做了两件事,第一件事就是通过 include $(BUILD_JAVA_LIBRARY)
语句将 src 和 proto 文件夹下的文件编译成了 am.jar。第二件事情就是通过 include $(BUILD_PREBUILT)
将 am 文件拷贝到手机目录中。并且通过 LOCAL_MODULE 属性将模块名定义为 am。
在编译 Android 源码的时候,系统会从 PRODUCT_PACKAGES 属性列表中读取需要编译合入的模块,在 build/make/target/product/base.mk 文件下,有以下语句将 am 加进了 PRODUCT_PACKAGES 属性当中,因此在源码编译时,会将 am 指令编译进去。当然,在源码环境下通过 mmm frameworks/base/cmds/am/ 指令也能对模块进行编译。
PRODUCT_PACKAGES += \
20-dns.conf \
95-configured \
org.apache.http.legacy.boot \
appwidget \
appops \
am \
android.policy \
android.test.base \
android.test.mock \
android.test.runner \
app_process \
applypatch \
audioserver \
bit \
blkid \
有了上面的知识,要自定义 adb 指令就显得容易很多了。假设我们有一个需求,需要自定义一个 adb 指令模拟按键输入,那么我们就可以写一个名为 testinput 的 shell 文件:
#!/system/bin/sh
input keyevent $1
接着写对应的 Android.mk:
# Copyright 2008 The Android Open Source Project
#
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := testinput
LOCAL_SRC_FILES := testinput
LOCAL_MODULE_CLASS := EXECUTABLES
LOCAL_MODULE_TAGS := optional
include $(BUILD_PREBUILT)
并且将 testinput 加入 build/make/target/product/base.mk 的 PRODUCT_PACKAGES 列表当中,当编译系统源码时,就会将 testinput 编译进 system/bin 中,我们就可以通过 adb 指令去使用它了。没有源码的朋友也可以用有 root 权限的机器或者模拟器来试验,通过以下指令将 testinput 文件 push 到 system/bin 目录进行使用。
adb root
adb remount
adb push <path>/testinput /system/bin
当然,上面的例子其实并没有什么实际的用处,仅仅是拐了个弯调用了 input 指令而已,为的是方便大家理解。
在实际应用中,自定义 adb 指令能够使得测试更加方便快捷,提高测试效率。我们可以通过自定义 adb 非常方便地调用上层或底层实现的功能。
比如在我司要求的音频测试中,我的实现思路是根据输入的指令启动指定的服务,并且在服务中根据传入的参数设置音频的输入输出端口和模式等。这样做的好处在于能大大缩小 adb 指令的长度和复杂度,并且对于不懂代码的测试人员比较友好,容易理解。
在使用 adb 的时候我们还有一些细节需要注意。我们都知道在 Android 8.0 及以上,启动一个没有正在运行的进程的服务需要使用 Context 的 startForegroundService 方法,并且服务启动后需要调用该服务的 startForeground 方法,否则会导致报错,这个限制在 adb 启动服务的情景中也是一样的。通过查看
frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
的以下代码我们很清楚的显示了这一点:
@Override
public int onCommand(String cmd) {
if (cmd == null) {
return handleDefaultCommands(cmd);
}
final PrintWriter pw = getOutPrintWriter();
try {
switch (cmd) {
case "start":
case "start-activity":
return runStartActivity(pw);
case "startservice":
case "start-service":
return runStartService(pw, false);
case "startforegroundservice":
case "startfgservice":
case "start-foreground-service":
case "start-fg-service":
return runStartService(pw, true);
case "stopservice":
case "stop-service":
return runStopService(pw);
.....
}
}
......
}
......
int runStartService(PrintWriter pw, boolean asForeground) throws RemoteException {
final PrintWriter err = getErrPrintWriter();
Intent intent;
try {
intent = makeIntent(UserHandle.USER_CURRENT);
} catch (URISyntaxException e) {
throw new RuntimeException(e.getMessage(), e);
}
if (mUserId == UserHandle.USER_ALL) {
err.println("Error: Can't start activity with user 'all'");
return -1;
}
pw.println("Starting service: " + intent);
pw.flush();
ComponentName cn = mInterface.startService(null, intent, intent.getType(),
asForeground, SHELL_PACKAGE_NAME, mUserId);
......
}
查看代码的 14 行和第 19 行我们可以发现,通过 adb 启动服务的时候都是通过 runStartService
方法启动的,而这个方法根据第二个布尔值参数来决定是否是启动前台服务,而第 45 行的 mInterface.startService
方法其实最终会调用到 AMS 的 startService
方法。
因此在启动一个没有运行中进程的服务时(比如在我司的音频测试需求中启动服务),我们需要通过 adb shell startforegroundservice
或者 adb shell startfgservice
才能正常运行。
再或者,在测试过程中,测试人员需要设置标志位对测试过的机器进行标记,此时我们一般会通过调用 adb shell setprop
和 adb shell getprop
方法设置属性。但是在设置的时候我们需要注意属性的开头需要是 persist.
,例如 persist.test
。如果属性的开头为 ro
,则该属性只能读,不能写,也就是 setprop
会无效。而如果没有特殊的开头,比如属性名仅仅是 test
,那么该属性在重启之后将不会保留。
本篇博客到这里就差不多了,如果有任何错漏欢迎提出交流,谢谢。